#!/usr/bin/python2.4

# C6304: copyright missing
# pylint: disable-msg=C6304

"""Models a time-varying 2-D vector field."""

import csv

import kdtree
from matrix import LatLon
from matrix import Polar
import timeformat


class CoordsToVector(object):
  """A single data point represenging a time varying vector field."""

  def __init__(self, index_time, latlon, value, attribution=None):
    """Constructor.

    Args:
      index_time: The number of seconds since the unix epoch.
      latlon: A LatLon for the point.
      value: A Polar for the value of the vector field at this point.
      attribution: A string with information about where this datum is from.
    """
    self._time = index_time  # seconds since unix epoch
    self._latlon = latlon
    self._value = value
    self._attribution = attribution

  def GetTime(self):
    return self._time

  def GetLatLon(self):
    return self._latlon

  def SetLatLon(self, latlon):
    self._latlon = latlon
    return self

  def GetValue(self):
    return self._value

  def SetValue(self, value):
    self._value = value
    return self

  def GetAttribution(self):
    return self._attribution

  def __str__(self):
    # Output textual calls to the constructor, so that we can exactly recreate
    # an instance of ourselves
    prtime = int(round(self._time))
    return 'CoordsToVector(%10d, %s, %s, attribution=%s)' % (
        prtime,
        self.GetLatLon().DMFormat(),
        self.GetValue().ConstructorFormat(),
        self.GetAttribution())

  def __eq__(self, x):
    return (
        self.GetTime() == x.GetTime()
        and self.GetLatLon() == x.GetLatLon()
        and self.GetValue() == x.GetValue())

  def FormatVerbose(self):
    prtime = int(round(self._time))
    return 'CoordsToVector(%10d {%s}, %s, %s, %s)' % (
        prtime,
        timeformat.FormatTime(prtime),
        self.GetLatLon().DMFormat(),
        self.GetValue().ConstructorFormat(),
        self.GetAttribution())

  def CSVFormat(self):
    return '%10d, %s, %s, "%s"' % (
        int(round(self._time)),
        self.GetLatLon().CSVFormat(),
        self.GetValue().CSVFormat(),
        self.GetAttribution())

  @staticmethod
  def BuildFromCSV(row, i):
    """Reads parsed CSV.

    Args:
      row: the vector of fields scanned by the CSV reader.
      i: The starting position

    Returns:
      A pair, where the first item in the pair is a CoordsToVector,
      and the second is the effective column number the caller wants to
      start reading at.
    """
    index_time = int(row[i+0])
    ll, i = LatLon.BuildFromCSV(row, i+1)
    value = Polar(float(row[i+1]), float(row[i+2]))
    attribution = row[i+3]
    cv = CoordsToVector(index_time, ll, value, attribution=attribution)
    if False:
      print 'read CoordsToVector of %s' % (cv,)
    return cv, i+3-1


class VectorField(object):
  """Models a time-varying 2-D vector field."""

  def __init__(self, name='unknown'):
    self._name = name
    self._times = {}  # maps unix epoch time to list of CoordsToVector.
    self._tree = {}  # maps unix epoch time to a KDTree instance.

    self._minlatlon = LatLon(float('inf'), float('inf'))
    self._maxlatlon = LatLon(float('-inf'), float('-inf'))

    self._latfuzz = 4  # MINUTES!
    self._lonfuzz = 4  # MINUTES!
    self._timefuzz = 2 * 3600  # 2 hours, as seconds

  def SetLatLonFuzzMinutes(self, latfuzz, lonfuzz):
    self._latfuzz = latfuzz
    self._lonfuzz = lonfuzz

  def SetTimeFuzzSeconds(self, time_fuzz_seconds):
    self._timefuzz = time_fuzz_seconds

  def Print(self, fd):
    """Prints this vector field to the file descriptor fd."""
    for time in self._times:
      for cv in self._times[time]:
        print >>fd, '%d (%s) ==> %s' % (
            time,
            timeformat.FormatTime(time),
            str(cv))
      kd = self._tree[time]
      print >>fd, '%d (%s) ++> %s' % (
          time,
          timeformat.FormatTime(time),
          str(kd))

  def AddItem(self, cv):
    """Add one point of data to the time varying vector field.

    Args:
      cv: An instance of a CoordsToVector
    """
    self._times.setdefault(cv.GetTime(), [])
    self._times[cv.GetTime()].append(cv)
    self.ExtendBoundingBox(cv.GetLatLon())

  def ReadFrom(self, file_name):
    """Read a csv file named file_name and add all entries to the field.

    There is no mechanism for adding additional points to the field after
    this is called, although that might be trivial to add.

    Args:
      file_name: The name of a file holding csv to read.
    """
    print 'Read vector field for %s from %s' % (self._name, file_name,)
    fd = open(file_name, 'rb')
    self._ReadFromFileHandle(fd)
    fd.close()

  def _ReadFromFileHandle(self, fd):
    reader = csv.reader(fd)
    for row in reader:
      cv, unused_nexti = CoordsToVector.BuildFromCSV(row, 0)
      self.AddItem(cv)
    self.BuildModelFromAllData()

  def ExtendBoundingBox(self, latlon):
    if latlon.lat < self._minlatlon.lat:
      self._minlatlon.lat = latlon.lat
    if latlon.lat > self._maxlatlon.lat:
      self._maxlatlon.lat = latlon.lat

    if latlon.lon < self._minlatlon.lon:
      self._minlatlon.lon = latlon.lon
    if latlon.lon > self._maxlatlon.lon:
      self._maxlatlon.lon = latlon.lon

  def LatLonInsideBoundingBox(self, index_time, latlon, trace=False):
    """Determines if latlon is in or very close outside of the bounding box.

    Args:
      index_time: The index time.
      latlon: The index coordinate.
      trace: Trace rejections.

    Returns:
      A boolean: True if inside the bounding box, False otherwise.
    """

    inbounds = (
        (self._minlatlon.lat - self._latfuzz <= latlon.lat) and
        (latlon.lat <= self._maxlatlon.lat + self._latfuzz) and
        (self._minlatlon.lon - self._lonfuzz <= latlon.lon) and
        (latlon.lon <= self._maxlatlon.lon + self._lonfuzz))
    if not inbounds and trace:
      print 'VF %8s at %s rejects latlon %s not in latlon %s ... %s' % (
          self._name,
          timeformat.FormatTime(index_time),
          latlon,
          self._minlatlon,
          self._maxlatlon)
    return inbounds

  def TimeInsideBoundingBox(self, index_time, latlon, trace=False):
    """Determines if index_time is in or very close outside of the bounding box.

    Args:
      index_time: The index time.
      latlon: The latlon index.
      trace: Trace rejections.

    Returns:
      A boolean: True if inside the bounding box, False otherwise.
    """
    inbounds = (
        (self._alltimes[0] - self._timefuzz <= index_time) and
        (index_time < self._alltimes[-1] + self._timefuzz))
    if not inbounds and trace:
      print 'VF %8s rejects time %s latlon %s not in %s ... %s' % (
          self._name,
          timeformat.FormatTime(index_time),
          latlon,
          timeformat.FormatTime(self._alltimes[0]),
          timeformat.FormatTime(self._alltimes[-1]))
    return inbounds

  def BuildModelFromAllData(self):
    """Builds the list of time points, used for the outermost index."""
    for index_time in self._times:
      points = []
      for cv in self._times[index_time]:
        points.append((cv.GetLatLon(), cv))
      self._tree[index_time] = kdtree.KDTree(2, points)
    self._alltimes = list(self._times)
    self._alltimes.sort()

  def Index(self, index_time, latlon, trace=False):
    polar, unused_attribution = self.FullIndex(index_time, latlon, trace=trace)
    return polar

  def FullIndex(self, index_time, latlon, trace=False):
    """Looks up and returns the value (a Polar) in the vector field.

    This will return Polar(0, 0) if the time or the latlon is not inside
    or near the field's bounding box.

    Args:
      index_time: The time part of the index, seconds since unix epoch.
      latlon: A LatLon part of the index.
      trace: If True, trace to stdout some actions.

    Returns:
      A Polar constructed by interpolation from items in the table.
    """
    if not self.TimeInsideBoundingBox(index_time, latlon, trace):
      return Polar(0, 0), None
    if not self.LatLonInsideBoundingBox(index_time, latlon, trace):
      return Polar(0, 0), None

    number_time_values = len(self._alltimes)
    if trace:
      print 'index_time=%d(%s) self._alltimes[0]=%d' % (
          index_time,
          timeformat.FormatTime(index_time),
          self._alltimes[0])

    if index_time <= self._alltimes[0]:
      t1 = self._alltimes[0]
      t2 = t1
    elif index_time >= self._alltimes[number_time_values-1]:
      t1 = self._alltimes[number_time_values-1]
      t2 = t1
    else:
      for i in xrange(0, number_time_values - 1):
        t1 = self._alltimes[i]
        t2 = self._alltimes[i+1]
        if t1 <= index_time and index_time < t2:
          break

    unused_p1, cv1, unused_d1 = self._tree[t1].NearestNeighbor(latlon)
    unused_p2, cv2, unused_d2 = self._tree[t2].NearestNeighbor(latlon)

    if t2 == t1:
      ratio = 0
    else:
      ratio = (index_time - t1) / (t2 - t1)

    magnitude = (
        cv1.GetValue().Mag()
        + (cv2.GetValue().Mag() - cv1.GetValue().Mag()) * ratio)
    angle = (
        cv1.GetValue().Angle()
        + (cv2.GetValue().Angle() - cv1.GetValue().Angle()) * ratio)

    avg = Polar(magnitude, angle)

    if trace:
      print '%10s Index time=%s latlon=%s cv1=%s' % (
          self._name, timeformat.FormatTime(index_time),
          latlon, cv1.FormatVerbose())
      print '%10s Index time=%s latlon=%s avg=%s' % (
          self._name, timeformat.FormatTime(index_time), latlon, avg)
      print '%10s Index time=%s latlon=%s cv2=%s' % (
          self._name, timeformat.FormatTime(index_time),
          latlon, cv2.FormatVerbose())

    return avg, cv1._attribution
